package com.squid.base.config.mybatis.interceptor;

import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.squid.base.utils.AsyncUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import java.sql.Statement;
import java.text.DateFormat;
import java.util.*;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Matcher;

/**
 * 慢SQL通知
 *
 * @author jinhang1
 * @date 2024/08/02
 */
@Intercepts({@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
        @Signature(type = StatementHandler.class, method = "batch", args = {Statement.class})})
@Slf4j
public class SlowSqlNotifyMybatisInterceptor implements Interceptor {
    private final MybatisIgnoreProperties mybatisIgnoreProperties;
    private final AtomicInteger count = new AtomicInteger();
    private final int threshold = 100000;

    public SlowSqlNotifyMybatisInterceptor(MybatisIgnoreProperties mybatisIgnoreProperties) {
        this.mybatisIgnoreProperties = mybatisIgnoreProperties;
    }

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 获取MappedStatement的ID
        MappedStatement statement = (MappedStatement) invocation.getArgs()[0];
        String statementId = statement.getId();
        // 计算这一次SQL执行钱后的时间，统计一下执行耗时
        long startTime = System.currentTimeMillis();
        Object proceed = invocation.proceed();
        long endTime = System.currentTimeMillis();
        long costTime = endTime - startTime;
        AtomicReference<String> printSql = new AtomicReference<>("");
        AtomicReference<String> error = new AtomicReference<>("");
        // 检查是否要跳过该Mapper
        if (isIgnored(statementId)) {
            return proceed; // 如果是需要跳过的Mapper，则直接返回结果
        }
        if (mybatisIgnoreProperties.getSlowQueryTime() > costTime) {
            return proceed;
        }
        ThreadPoolExecutor executor = null;
        try {
            executor = SpringUtil.getBean("slowQuerySqlNotifyExecutor", ThreadPoolExecutor.class);
        } catch (Exception e) {
            log.error("获取慢Sql告警的线程池失败", e);
            return proceed;
        }
        AsyncUtils.asyncVoid(() -> {
            try {
                // 通过generateSql方法拿到最终生成的SQL
                printSql.set(generateSql(invocation));
            } catch (Exception exception) {
                log.error("获取sql异常", exception);
                error.set(exception.getMessage());
            } finally {
                // 拼接日志打印过程
                String content = StrUtil.format("执行SQL耗时：{}ms 执行sql的方式：{}\n执行SQL：{}", costTime, statementId, printSql.get());
                if (StringUtils.hasText(error.get())) {
                    content = content + "-------error：" + error.get();
                }
                try {
                    log.info("慢SQL通知：{}", content);
                } catch (Exception e) {
                    log.error("Mybatis拦截器发送邮件报错了", e);
                }
                int num = count.incrementAndGet();
                if (num > threshold) {
                    //发给统计平台
                    count.set(0);
                    System.out.println("我发给了统计平台");
                }
            }
        }, executor);
        return proceed;
    }
    private static String generateSql(Invocation invocation) {
        // 获取到BoundSql以及Configuration对象
        // BoundSql 对象存储了一条具体的 SQL 语句及其相关参数信息。
        // Configuration 对象保存了 MyBatis 框架运行时所有的配置信息
        MappedStatement statement = (MappedStatement) invocation.getArgs()[0];
        Object parameter = null;
        if (invocation.getArgs().length > 1) {
            parameter = invocation.getArgs()[1];
        }
        Configuration configuration = statement.getConfiguration();
        BoundSql boundSql = statement.getBoundSql(parameter);

        // 获取参数对象
        Object parameterObject = boundSql.getParameterObject();
        // 获取参数映射
        List<ParameterMapping> params = boundSql.getParameterMappings();
        // 获取到执行的SQL
        String sql = boundSql.getSql();
        // SQL中多个空格使用一个空格代替
        sql = sql.replaceAll("[\\s]+", " ");
        if (!ObjectUtils.isEmpty(params) && !ObjectUtils.isEmpty(parameterObject)) {
            // TypeHandlerRegistry 是 MyBatis 用来管理 TypeHandler 的注册器。TypeHandler 用于在 Java 类型和 JDBC 类型之间进行转换
            TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
            // 如果参数对象的类型有对应的 TypeHandler，则使用 TypeHandler 进行处理
            if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(getParameterValue(parameterObject)));
            } else {
                // 否则，逐个处理参数映射
                for (ParameterMapping param : params) {
                    // 获取参数的属性名
                    String propertyName = param.getProperty();
                    MetaObject metaObject = configuration.newMetaObject(parameterObject);
                    // 检查对象中是否存在该属性的 getter 方法，如果存在就取出来进行替换
                    if (metaObject.hasGetter(propertyName)) {
                        Object obj = metaObject.getValue(propertyName);
                        sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(getParameterValue(obj)));
                        // 检查 BoundSql 对象中是否存在附加参数。附加参数可能是在动态 SQL 处理中生成的，有的话就进行替换
                    } else if (boundSql.hasAdditionalParameter(propertyName)) {
                        Object obj = boundSql.getAdditionalParameter(propertyName);
                        sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(getParameterValue(obj)));
                    } else {
                        // 如果都没有，说明SQL匹配不上，带上“缺失”方便找问题
                        sql = sql.replaceFirst("\\?", "缺失");
                    }
                }
            }
        }
        return sql;
    }

    private static String getParameterValue(Object object) {
        String value = "";
        if (object instanceof String) {
            value = "'" + object.toString() + "'";
        } else if (object instanceof Date) {
            DateFormat format = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.CHINA);
            value = "'" + format.format((Date) object) + "'";
        } else if (!ObjectUtils.isEmpty(object)) {
            value = object.toString();
        }
        return value;
    }
    private boolean isIgnored(String statementId) {
        // 检查statementId是否包含需要忽略的Mapper的名称
        Set<String> ignoredMappers = mybatisIgnoreProperties.getIgnoredMappers();
        for (String ignoredMapper : ignoredMappers) {
            if (statementId.startsWith(ignoredMapper)) {
                return true;
            }
        }
        return false;
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        // 可以通过properties配置插件参数
    }

}

